############################################################
#                                                          #
# The implementation of PHPRPC Protocol 3.0                #
#                                                          #
# GoogleAppEngine.py                                       #
#                                                          #
# Release 3.0.1                                            #
# Copyright (c) 2005-2008 by Team-PHPRPC                   #
#                                                          #
# WebSite:  http://www.phprpc.org/                         #
#           http://www.phprpc.net/                         #
#           http://www.phprpc.com/                         #
#           http://sourceforge.net/projects/php-rpc/       #
#                                                          #
# Authors:  Ma Bingyao <andot@ujn.edu.cn>                  #
#                                                          #
# This file may be distributed and/or modified under the   #
# terms of the GNU Lesser General Public License (LGPL)    #
# version 3.0 as published by the Free Software Foundation #
# and appearing in the included file LICENSE.              #
#                                                          #
############################################################
#
# PHPRPC library for Google App Engine.
#
# Copyright (C) 2005-2008 Ma Bingyao <andot@ujn.edu.cn>
# Version: 3.0
# LastModified: Oct 7, 2008
# This library is free.  You can redistribute it and/or modify it.


import base64, md5, phpformat, random, sys, xxtea
from google.appengine.api import urlfetch
from google.appengine.ext.webapp import Response

class PHPRPC_Error(Exception):
    def __init__(self, number, message):
        self.number = number
        self.message = message
    def __str__(self):
        return "%i:%s" % (self.number, self.message)

class _Method:
    def __init__(self, invoke, name):
        self.__invoke = invoke
        self.__name = name
    def __call__(self, *args):
        return self.__invoke(self.__name, args)

class _Proxy:
    def __init__(self, invoke):
        self.__invoke = invoke
    def __getattr__(self, name):
        return _Method(self.__invoke, name)
    
class PHPRPC_Client:
    def __init__(self, url = None):
        if url != None:
            self.__url = url
        else:
            self.__url = None
        self.__username = None
        self.__password = None
        self.__key = None
        self.__keylength = 128
        self.__encryptmode = 0
        self.__cookie = ''
        self.__cookies = {}
        self.charset = 'utf-8'
        self.timeout = 30
        self.output = ''
        self.warning = None
        self.__server_version = None
        
    def __getattr__(self, name):
        return _Method(self.invoke, name)

    def invoke(self, name, args, byRef = False):
        try:
            self.__key_exchange()
            result = self.__post("phprpc_func=%s&phprpc_args=%s&phprpc_encrypt=%s&phprpc_ref=%s" % (
                name,
                base64.b64encode(self.__encrypt(phpformat.serialize(args), 1)).replace('+', '%2B'),
                self.__encryptmode,
                str(byRef).lower()
            ))
            if result.has_key('phprpc_errstr') and result.has_key('phprpc_errno'):
                self.warning = PHPRPC_Error(int(result['phprpc_errno']), base64.b64decode(result['phprpc_errstr']))
            elif result.has_key('phprpc_functions'):
                self.warning = PHPRPC_Error(1, "PHPRPC server haven't received then POST data!")
            else:
                self.warning = PHPRPC_Error(1, "PHPRPC server occured unknown error!")
            if result.has_key('phprpc_output'):
                self.output = base64.b64decode(result['phprpc_output'])
                if self.__server_version >= 3: self.output = self.__decrypt(self.output, 3)
            else:
                self.output = ''
            if result.has_key('phprpc_result'):
                if result.has_key('phprpc_args'):
                    arguments = phpformat.unserialize(self.__decrypt(base64.b64decode(result['phprpc_arg']), 1))
                    for key in arguments: args[key] = arguments[key]
                return phpformat.unserialize(self.__decrypt(base64.b64decode(result['phprpc_result']), 2))
            else:
                return self.warning
        except PHPRPC_Error, e:
            return e
        except Exception, e:
            e = PHPRPC_Error(1, str(e))
            return e

    def useService(self, url = None, username = None, password = None):
        if url != None:
            self.close()
            self.__url = url
            self.__key = None
            self.__keylength = 128
            self.__encryptmode = 0
            self.__cookie = ''
            self.__cookies = {}
            self.charset = 'utf-8'
        self.__username = username
        self.__password = password
        return _Proxy(self.invoke)

    def keylength():
        def fget(self):
            return self.__keylength
        def fset(self, value):
             if self.__key == None:
                 self.__keylength = value
        return locals()

    keylength = property(**keylength())
    
    def encryptmode():
        def fget(self):
            return self.__encryptmode
        def fset(self, value):
            if 0 <= value <= 3:
                self.__encryptmode = int(value)
            else:
                self.__encryptmode = 0
        return locals()
    
    encryptmode = property(**encryptmode())
            
    def __post(self, req):
        headers = {
            'User-Agent' : "PHPRPC 3.0 Client for Python (%s)" % sys.version,
            'Cache-Control' : 'no-cache',
            'Content-Type' : "application/x-www-form-urlencoded; charset=%s" % self.charset,
            'Connection' : 'keep-alive',
        }
        if self.__cookie != '':
            headers['Cookie'] = self.__cookie
        if (self.__username != None) and (self.__password != None):
            headers['Authorization'] = 'Basic %s' % base64.b64encode('%s:%s' %
                (self.__username, self.__password))
        resp = urlfetch.fetch(self.__url, req, 'POST', headers)
        data = resp.content
        if resp.status_code == 200:
            if resp.headers.has_key('x-powered-by'):
                x_powered_by = resp.headers['x-powered-by']
                self.__server_version = None
                for value in x_powered_by.split(','):
                    value = value.strip()
                    if value[0:13] == 'PHPRPC Server':
                        self.__server_version = float(value[14:])
                if self.__server_version == None:
                    raise PHPRPC_Error(1, 'Illegal PHPRPC server.')
            else:
                raise PHPRPC_Error(1, 'Illegal PHPRPC server.')
            if resp.headers.has_key('content-type'):
                content_type = resp.headers['content-type']
                for value in content_type.split(','):
                    value = value.strip()
                    if value[0:20] == 'text/plain; charset=':
                        self.charset = value[20:]
            if resp.headers.has_key('set-cookie'):
                set_cookie = resp.headers['set-cookie']
                for value in set_cookie.split(','):
                    for pairs in value.split(';'):
                        pairs = pairs.strip()
                        pair = pairs.split('=', 1)
                        name = pair[0]
                        if pair[0] not in ('domain', 'expires', 'path', 'secure'):
                            self.__cookies[pair[0]] = pair
                self.__cookie = '; '.join(
                    ('='.join(self.__cookies[name]) for name in self.__cookies)
                )
            result = {}
            for line in data.split(";\r\n"):
                if line != '':
                    (key, value) = line.split('=', 1)
                    result[key] = value[1:-1]
            return result
        else:
            raise PHPRPC_Error(resp.status_code, Response.http_status_message(resp.status_code))

    def __key_exchange(self):
        if (self.__key == None) and (self.__encryptmode != 0):
            result = self.__post("phprpc_encrypt=true&phprpc_keylen=%i" % self.__keylength)
            self.__keylength = int(result['phprpc_keylen']) if result.has_key('phprpc_keylen') else 128
            if result.has_key('phprpc_encrypt'):
                encrypt = phpformat.unserialize(base64.b64decode(result['phprpc_encrypt']))
                x = random.randint(1 << (self.__keylength - 2), 1 << (self.__keylength - 1))
                key = pow(long(encrypt['y']), x, long(encrypt['p']))
                if self.__keylength == 128:
                    key = hex(key)[2:-1].rjust(32, '0')
                    self.__key = ''.join((chr(int(key[i*2:i*2+2], 16)) for i in xrange(16)))
                else:
                    self.__key = md5.new(str(key)).digest()
                y = pow(long(encrypt['g']), x, long(encrypt['p']))
                self.__post('phprpc_encrypt=%s' % y)
            else:
                self.__key = None
                self.__encryptmode = 0
                
    def __encrypt(self, s, level):
        return (xxtea.encrypt(s, self.__key)
            if ((self.__key != None) and (self.__encryptmode >= level))
            else s
        )
    
    def __decrypt(self, s, level):
        return (xxtea.decrypt(s, self.__key)
            if ((self.__key != None) and (self.__encryptmode >= level))
            else s
        )
    
if __name__ == '__main__':
    client = PHPRPC_Client('http://bawbaw.icittys.com/rpc_server.php')
    proxy = client.useService()
    client.keylength = 256
    client.encryptmode = 1
    print client.add(134567876543, 2456784587654)
    print proxy.add(134567876543, 2456784587654)
